package racer.client.gui;

import java.awt.Canvas;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Image;
import java.awt.Toolkit;
//import java.awt.Graphics;
import java.awt.Graphics2D;
//import java.awt.Image;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.font.TextAttribute;
import java.awt.geom.AffineTransform;
import java.awt.geom.Area;
import java.awt.geom.Point2D;
import java.awt.image.*;

import javax.swing.JFrame;
import javax.swing.JPanel;

import racer.client.gui.entities.CarEntity;
import racer.client.gui.entities.Entity;
import racer.client.gui.tiles.TiledMap;
import racer.client.network.NetworkThreadManager;

import java.io.*;
import java.text.AttributedString;
import java.util.HashMap;
import java.util.Iterator;



public class Game extends Canvas implements Runnable {

	// not important for us, just to please eclipse who warns about this variable not being declared
	// to do with serialization
	private static final long serialVersionUID = 1;

	private static final int NO_DELAYS_PER_YIELD = 16;
	private static final int MAX_FRAME_SKIPS = 5;
	private static int AVERAGE_FPS_NUMBER = 10;
	private static final int SCREEN_WIDTH = 1024;
	private static final int SCREEN_HEIGHT = 900;
	public static boolean debug = false;
	private int fps;
	private String mapName, theme;

	private static NetworkThreadManager networklayer;
	private TiledMap map;
	private Gametype gametype;
	private Area mapCollIntersection;
	private Area finishCollIntersection;
	private boolean gameOver = false;
	private boolean running = false;
	private boolean gameStarted = false;

	//private VolatileImage dbImage;
	//private Graphics2D dbg2d;
	private BufferStrategy strategy;
	private double zoomLevel;

	private Entity player;
	private HashMap<Integer, Entity> entities;

	private int period;
	private long gameStartTime, totalElapsedTime=0L, prevStatsTime, statsInterval=0L, frameCount, statsCount = 0;
	private int gameTime = 0;
	private long framesSkipped = 0L;
	private long totalFramesSkipped = 0L;
	private double fpsStore[] = new double[AVERAGE_FPS_NUMBER];
	private double upsStore[] = new double[AVERAGE_FPS_NUMBER];
	private double averageFPS = 0.0;
	private double averageUPS = 0.0;

	public Game(int fps, NetworkThreadManager networklayer, String mapName, Gametype gametype, String theme) {
		this.networklayer = networklayer;
		networklayer.setGame(this);
		this.fps = fps;
		this.mapName = mapName;
		this.gametype = gametype;
		this.theme = theme;
	}
	
	public void run() {
			
		initFrame();

		//buffer strategy die AWT toelaat om de graphics te versnellen
		createBufferStrategy(2);
		strategy = getBufferStrategy();

		new ImageBank(theme);
		map = new TiledMap(SCREEN_WIDTH, SCREEN_HEIGHT);
		//gametype = new Gametype(4, 1000, 3);
		player = new CarEntity(-1, 30, 30, ImageBank.getImage("car1"));
		player.setAngle(0);
		entities = new HashMap<Integer, Entity>();

		try {
			map.buildMap(new File("maps/" + mapName));
		} catch (IOException e) {
			e.printStackTrace();
		}
		
		/*
		networklayer = new NetworkThreadManager(2222,"localhost", this);
		String nickid="1";
		networklayer.setSetting("nickid",nickid);
		networklayer.setMessageIDs();
		networklayer.sendListRacesRequest();
		*/

		long beforeTime, afterTime, timeDiff, sleepTime;
		long overSleepTime = 0L;
		int noDelays = 0;
		long excess = 0L;
		period = 1000000000/this.fps;	// in nanoseconds
		gameStartTime = System.nanoTime();
		beforeTime = gameStartTime;
		prevStatsTime = gameStartTime;

		/*
		 * Animation framework: update - render - sleep
		 * update: move the players on the screen, calculate some stuff ...
		 * render: draws everything using double buffering
		 * sleep: allows other things to execute, so update-render doesn't hog all the cpu power
		 */
		running = true;
		while(!gameOver) {
			gameUpdate(System.nanoTime() - beforeTime);
			//System.out.println("Timediff update: " + (int)(System.nanoTime()-beforeTime)/1000000 + "ms");
			gameRender();
			//System.out.println("Timediff render: " + (int)(System.nanoTime()-beforeTime)/1000000 + "ms");
			paintScreen();
			//System.out.println("Timediff paint: " + (int)(System.nanoTime()-beforeTime)/1000000 + "ms");

			afterTime = System.nanoTime();
			timeDiff = afterTime - beforeTime;
			//System.out.println("timeDiff: " + timeDiff/1000000 + "ms");
			sleepTime = (period - timeDiff) - overSleepTime;

			if(sleepTime > 0) {
				try { 
					// nanoseconds to ms
					Thread.sleep(sleepTime/1000000L); 
				} catch (InterruptedException e) {
				}
				// fix sleep() inaccuracy
				overSleepTime = (System.nanoTime() - afterTime) - sleepTime;
			} else {
				// frame took longer than the period
				excess -= sleepTime;
				overSleepTime = 0L;
				// if there hasn't been slept in a number of sequential frames, pause this thread
				// and allow other thread to execute
				if (++noDelays >= NO_DELAYS_PER_YIELD) {
					Thread.yield();
					noDelays = 0;
				}
			}
			beforeTime = System.nanoTime();

			/* 
			 * Frame animation taking too long => calls to gameUpdate() without rendering the updates
			 * this can cause jumps in the game if too many updates are not rendered
			 * When should this happen? If update and render are too slow to match the desired FPS, 
			 */
			int skips = 0;
			while( (excess > period) && (skips < MAX_FRAME_SKIPS)) {
				excess -= period;
				gameUpdate(System.nanoTime() - beforeTime);
				beforeTime = System.nanoTime();
				skips++;
			}
			framesSkipped += skips;
			storeStats();
		} // end while
		// game over, show some messages
		Graphics2D dbg2d = (Graphics2D)strategy.getDrawGraphics();
		dbg2d.setFont(new Font("Arial Bold", 0, 26));
		dbg2d.drawString("GAME OVER", SCREEN_WIDTH/2, SCREEN_HEIGHT/2);
		//dbg2d.draw3DRect(SCREEN_HEIGHT/2, SCREEN_WIDTH/2, 100, 100, true);
		/*AttributedString as = new AttributedString("Game Over");
        Font font = new Font("serif", Font.PLAIN, 50);
        as.addAttribute(TextAttribute.FONT, font, 0, 8);
        dbg2d.drawString(as, 400, 400);
        */
		while(running) {
			paintScreen();
		}
	}

	private void initFrame() {
		JFrame container = new JFrame("Racer");

		JPanel panel = (JPanel) container.getContentPane();
		panel.setPreferredSize(new Dimension(SCREEN_WIDTH, SCREEN_HEIGHT));
		panel.setLayout(null);

		setBounds(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
		panel.add(this);

		// AWT moet het scherm niet hertekenen, dit doen we zelf
		setIgnoreRepaint(true);

		container.pack();
		container.setResizable(true);
		container.setVisible(true);

		container.addWindowListener(new WindowAdapter() {

			@Override
			public void windowClosing(WindowEvent e) {
				System.exit(0);
			}
		});

		addKeyListener(new InputHandler());

		requestFocus();
	}

	private void gameUpdate(long elapsedTime) {
		player.move(elapsedTime);
		map.collisionCheck(player);
		mapCollIntersection = map.collisionCheck2(player);
		finishCollIntersection = map.collisionWithFinishLine(player);
		
		// car collision
		for(int i=0; i<entities.size(); i++) {
			
		}
		if(gameStarted) {
			if(map.crossedFinish(player)) {
				gametype.addLap();
			}
			//gameTime = (int)((currentTime - gameStartTime)/1000000000L);
			//gametype.addTime((int)((elapsedTime)/1000000L));
			gametype.setTime(gameTime);
		
			gameOver = gametype.isFinished();
			if(gameOver)
				networklayer.sendGameOverMessage();
		}
		networklayer.sendPositionToServer(player.getX(),player.getY(),player.getAngle(),-1);
	}

	/**
	 * Draw game elements to double buffer image
	 *
	 */
	private void gameRender() {
		Graphics2D dbg2d = (Graphics2D)strategy.getDrawGraphics();
		/*if(dbImage == null) {
			//dbImage = createImage(SCREEN_WIDTH, SCREEN_HEIGHT);
			dbImage = createVolatileImage(SCREEN_WIDTH, SCREEN_HEIGHT);
			if (dbImage == null) {

			} else {
				dbg2d = (Graphics2D)dbImage.getGraphics();
			}
		}*/
		// draw map
		map.drawMap(dbg2d, player);
		// draw player
		player.drawPic(dbg2d);
		
		if(finishCollIntersection != null) {
			dbg2d.setColor(Color.RED);
			finishCollIntersection.transform(map.mapToScreenTransform());
			dbg2d.fill(finishCollIntersection);			
		}
		
		// draw opponents and perhaps other movable entities
		Iterator it = entities.keySet().iterator();
		while(it.hasNext()) {
			Entity ent = entities.get(it.next());
			Point2D.Double p2d = (Point2D.Double)map.mapToScreenPosition(new Point2D.Double(ent.getX(), ent.getY()));
			ent.drawPic(dbg2d, (int)p2d.getX(), (int)p2d.getY());
			Area entArea = new Area(player.getShape());
			Area oppArea = new Area(ent.getShape());
			Area carCollArea = (Area)entArea.clone();
			carCollArea.intersect(oppArea);
			carCollArea.transform(map.mapToScreenTransform());
			dbg2d.setColor(Color.GREEN);
			dbg2d.fill(carCollArea);
			//System.out.println("   " + ent.getX() + "   " + ent.getY());
		}
		
		if(debug) {
			dbg2d.setColor(Color.RED);
			dbg2d.drawString(averageFPS + " / " + averageUPS, 20, 120);
			dbg2d.drawString("Game Time: " + gameTime, 20, 140);
			dbg2d.drawString("Total elapsed time: " + totalElapsedTime, 20, 160);
			dbg2d.drawString("Total frames skipped: " + totalFramesSkipped, 20, 180);
			dbg2d.drawString("Crossed Finishline: " + map.crossedFinish(player), 20, 200);
	
			// debug crosses on each corner of vehicle
			Point2D.Double p2d_1 = (Point2D.Double)map.mapToScreenPosition((Point2D.Double)player.getTopRightCornerPosition());
			Point2D.Double p2d_2 = (Point2D.Double)map.mapToScreenPosition((Point2D.Double)player.getTopLeftCornerPosition());
			Point2D.Double p2d_3 = (Point2D.Double)map.mapToScreenPosition((Point2D.Double)player.getBottomLeftCornerPosition());
			Point2D.Double p2d_4 = (Point2D.Double)map.mapToScreenPosition((Point2D.Double)player.getBottomRightCornerPosition());	
			dbg2d.setColor(Color.CYAN);
			dbg2d.drawLine((int)p2d_1.getX(), (int)p2d_1.getY()-5, (int)p2d_1.getX(), (int)p2d_1.getY()+5);
			dbg2d.drawLine((int)p2d_1.getX()-5, (int)p2d_1.getY(), (int)p2d_1.getX()+5, (int)p2d_1.getY());
			dbg2d.setColor(Color.BLUE);
			dbg2d.drawLine((int)p2d_2.getX(), (int)p2d_2.getY()-5, (int)p2d_2.getX(), (int)p2d_2.getY()+5);
			dbg2d.drawLine((int)p2d_2.getX()-5, (int)p2d_2.getY(), (int)p2d_2.getX()+5, (int)p2d_2.getY());
			dbg2d.setColor(Color.RED);
			dbg2d.drawLine((int)p2d_3.getX(), (int)p2d_3.getY()-5, (int)p2d_3.getX(), (int)p2d_3.getY()+5);
			dbg2d.drawLine((int)p2d_3.getX()-5, (int)p2d_3.getY(), (int)p2d_3.getX()+5, (int)p2d_3.getY());
			dbg2d.setColor(Color.WHITE);
			dbg2d.drawLine((int)p2d_4.getX(), (int)p2d_4.getY()-5, (int)p2d_4.getX(), (int)p2d_4.getY()+5);
			dbg2d.drawLine((int)p2d_4.getX()-5, (int)p2d_4.getY(), (int)p2d_4.getX()+5, (int)p2d_4.getY());
	
			// debug cross showing the center of the screen
			dbg2d.setColor(Color.WHITE);
			dbg2d.drawLine(SCREEN_WIDTH/2, 0, SCREEN_WIDTH/2, SCREEN_HEIGHT);
			dbg2d.drawLine(0, SCREEN_HEIGHT/2, SCREEN_WIDTH, SCREEN_HEIGHT/2);
			
			if(mapCollIntersection != null) {
				dbg2d.setColor(Color.YELLOW);
				mapCollIntersection.transform(map.mapToScreenTransform());
				dbg2d.fill(mapCollIntersection);
			}
		} else {
			dbg2d.setColor(Color.RED);
			dbg2d.drawString("Game Time: " + gameTime, 20, 20);
			dbg2d.drawString("FPS: " + averageFPS, 20, 40);
		}
		// waiting for players
		if(!gameStarted) {
			dbg2d.setColor(Color.RED);
			dbg2d.setFont(new Font("Arial Bold", 0, 26));
			dbg2d.drawString("WAITING FOR " + (gametype.getMaxPlayers()-entities.size()-1) + " PLAYER(S)", SCREEN_WIDTH/2, SCREEN_HEIGHT/2);
		}
	}

	private void paintScreen() {
		Graphics2D g2d;
		try {
			g2d = (Graphics2D)strategy.getDrawGraphics();
			g2d.scale(50, 50);
			//if (g2d != null && dbImage != null) {
				//g2d.drawImage(dbImage, 0, 0, null);
				// Ensures that the display is promptly updated. This is required for Linux, which 
				// doesn't automatically flush its display buffer. Without it, the animation may be 
				// only partially updated, creating a tearing effect (Killer Game Programming in Java, 
				// Andrew Davison, O'Reilly, page 22
				Toolkit.getDefaultToolkit().sync();
				g2d.dispose();
				strategy.show();
			//}
		} catch (Exception e) {

		}
	}

	private void storeStats() {
		frameCount++;
		statsInterval += period;

		long currentTime = System.nanoTime();
		gameTime = (int)((currentTime - gameStartTime)/1000000000L);	//ns to seconds

		long realElapsedTime = currentTime - prevStatsTime;
		totalElapsedTime += realElapsedTime;

		totalFramesSkipped += framesSkipped;

		// compute the latest FPS and UPS
		double actualFPS = 0;
		double actualUPS = 0;
		if(totalElapsedTime > 0) {
			actualFPS = (((double)frameCount / totalElapsedTime)*1000000000L);
			actualUPS = (((double)(frameCount + totalFramesSkipped) / totalElapsedTime) * 1000000000L);
		}

		fpsStore[ (int)statsCount%AVERAGE_FPS_NUMBER ] = actualFPS;
		upsStore[ (int)statsCount%AVERAGE_FPS_NUMBER ] = actualUPS;		
		statsCount++;

		double totalFPS = 0.0;
		double totalUPS = 0.0;
		for(int i=0; i<AVERAGE_FPS_NUMBER; i++) {
			totalFPS += fpsStore[i];
			totalUPS += upsStore[i];
		}

		if(statsCount < AVERAGE_FPS_NUMBER) {
			averageFPS = totalFPS / statsCount;
			averageUPS = totalUPS / statsCount;
		} else {
			averageFPS = totalFPS / AVERAGE_FPS_NUMBER;
			averageUPS = totalUPS / AVERAGE_FPS_NUMBER;
		}

		framesSkipped = 0;
		prevStatsTime = currentTime;
		statsInterval = 0L;
	}

	public void updateOpponent(int id, double x, double y, int angle) {
		if(entities != null) {
			Entity ent = entities.get(id);
			if(ent==null) {
				Entity newEnt = new CarEntity(-1, (int)x, (int)y, ImageBank.getImage("car1"));
				newEnt.setAngle(angle);
				entities.put(id, newEnt);
			} else{
				//Point2D.Double p2d = (Point2D.Double)map.mapToScreenPosition(new Point2D.Double(x,y));
				//ent.setXY(p2d.getX(), p2d.getY());
				ent.setXY(x,y);
				ent.setAngle(angle);
			}
		}
	}
	
	public void setGameOver(boolean isgameover) {
		this.gameOver = isgameover;
	}
	public void setGameStarted(boolean gameStarted) {
		/*while(!running) {
			Thread.yield();
		}*/
		Graphics2D dbg2d = (Graphics2D)strategy.getDrawGraphics();
		dbg2d.setColor(Color.RED);
		dbg2d.setFont(new Font("Arial Bold", 0, 26));
		int countdown = 5;
		dbg2d.drawString("STARTING RACE IN " + countdown + " SECONDS", SCREEN_WIDTH/2, SCREEN_HEIGHT/2);
		while(countdown > 0) {
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				
			}
			countdown--;
			dbg2d.drawString("STARTING RACE IN " + countdown + " SECONDS", SCREEN_WIDTH/2, SCREEN_HEIGHT/2);
			
		}
		
		this.gameStarted = gameStarted;
		gameStartTime = System.nanoTime();
		// reset positions		// draw opponents and perhaps other movable entities
		player.setToInitPosition();
	}

	private class InputHandler extends KeyAdapter {
		public void keyPressed(KeyEvent e) {
			//System.out.println("Key pressed");
			if (e.getKeyCode() == KeyEvent.VK_LEFT) {
				player.setSteeringLeft(true);
			}
			if (e.getKeyCode() == KeyEvent.VK_RIGHT) {
				player.setSteeringRight(true);
			}
			if (e.getKeyCode() == KeyEvent.VK_UP) {
				player.setAccelerating(true);
			}
			if (e.getKeyCode() == KeyEvent.VK_DOWN) {
				player.setBraking(true);
			}
			if(e.getKeyCode() == KeyEvent.VK_D) {
				debug = !debug;
				TiledMap.debug = debug;
			}
			if(e.getKeyCode() == KeyEvent.VK_1) {
				int angle = player.getAngle();
				player = new CarEntity(-1, (int)player.getX(), (int)player.getY(), ImageBank.getImage("car1"));
				player.setAngle(angle);
			}
			if(e.getKeyCode() == KeyEvent.VK_2) {
				int angle = player.getAngle();
				player = new CarEntity(-1, (int)player.getX(), (int)player.getY(), ImageBank.getImage("car2"));
				player.setAngle(angle);
			}
			if(e.getKeyCode() == KeyEvent.VK_3) {
				int angle = player.getAngle();
				player = new CarEntity(-1, (int)player.getX(), (int)player.getY(), ImageBank.getImage("car3"));
				player.setAngle(angle);
			}
			if(e.getKeyCode() == KeyEvent.VK_R)
				player.setXY(30, 30);
			if(e.getKeyCode() == KeyEvent.VK_5) {
				zoomLevel--;
				if(zoomLevel < 1)
					zoomLevel = 1;
			}
			if(e.getKeyCode() == KeyEvent.VK_6) {
				zoomLevel++;
			}
			if(e.getKeyCode() == KeyEvent.VK_7) {
				fps+=5;
			}
			if(e.getKeyCode() == KeyEvent.VK_8) {
				fps-=5;
			}
				
		}

		public void keyReleased(KeyEvent e) {
			if (e.getKeyCode() == KeyEvent.VK_UP)
				player.setAccelerating(false);
			if (e.getKeyCode() == KeyEvent.VK_DOWN)
				player.setBraking(false);
			if (e.getKeyCode() == KeyEvent.VK_LEFT)
				player.setSteeringLeft(false);
			if (e.getKeyCode() == KeyEvent.VK_RIGHT)
				player.setSteeringRight(false);
		}
	}

	/*
	public static void main(String[] args) {
		new Game(30);
	}
	*/

}
